#include <iostream>
#include <fstream>
#include <vector>
#include <cstdlib>
#include <cstdio>
#include <windows.h>
#include "ftplib.h"

using namespace std;

#define WM_TRAYICON (WM_USER + 1)
#define WM_PERCENT (WM_USER + 2)
#define WM_STATUS (WM_USER + 3)
#define READY 1
#define TRANSFER 2
#define FAILED 3

struct FILE_INFO {
    WIN32_FIND_DATA findData;
    string fullPath;
};

void recordCheckTime();
FILETIME readLastCheckTime();
string stringForFileTime(FILETIME in);
bool fileTimeEmpty(FILETIME in);
inline bool fileModifiedAfterTime(FILE_INFO file, FILETIME time);
bool storeDirectoryContents(string, vector<FILE_INFO>*, vector<FILE_INFO>*);
void printFindInfoVector(vector<FILE_INFO>);
void recordCheckTime();
FILETIME readLastCheckTime();
vector<FILE_INFO> filesModifiedAfterTime(vector<FILE_INFO> files, FILETIME time);
netbuf* connectToServer();
void disconnectFromServer(netbuf* nControl);
string readInTempFile();
void replicateDirectoryStructure(vector<FILE_INFO> dirs, string origin, netbuf* nControl);
void pushChangedFiles(vector<FILE_INFO> files, string origin, netbuf* nControl, NOTIFYICONDATA* nid);
bool updateRemoteFiles(vector<FILE_INFO> modifiedFiles, vector<FILE_INFO> directories, string origin, NOTIFYICONDATA* nid);
void performUpdatedFilesRoutine(string origin, NOTIFYICONDATA* nid);
DWORD WINAPI loopProcedure(void* nidV);
LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);
HICON loadIconFromBitmap(string file);
void loadSettingsFile();

HICON readyIcon;
HICON transferIcon;
HICON failedIcon;
string origin;
string server;
string username;
string password;
ofstream logfile;

int WINAPI WinMain(HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nCmdShow)
{
    HWND hwnd;
    MSG messages;
    WNDCLASSEX wincl;

    logfile.open("logfile.txt");

    readyIcon = loadIconFromBitmap("ready.bmp");
    transferIcon = loadIconFromBitmap("transfer.bmp");
    failedIcon = loadIconFromBitmap("failed.bmp");

    wincl.hInstance = hThisInstance;
    wincl.lpszClassName = "Frame";
    wincl.lpfnWndProc = WindowProcedure;
    wincl.style = CS_DBLCLKS;
    wincl.cbSize = sizeof (WNDCLASSEX);

    wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
    wincl.lpszMenuName = NULL;
    wincl.cbClsExtra = 0;
    wincl.cbWndExtra = 0;
    wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;

    if (!RegisterClassEx (&wincl))
        return 0;

    hwnd = CreateWindowEx(0, "Frame", "", 0, 0, 0, 100, 100, HWND_DESKTOP, NULL, hThisInstance, NULL);
    ShowWindow (hwnd, SW_HIDE);

    NOTIFYICONDATA nid;
    ZeroMemory(&nid,sizeof(nid));
    nid.cbSize				=	sizeof(NOTIFYICONDATA);
    nid.hWnd				=	hwnd;
    nid.uID					=	0;
    nid.uFlags				=	NIF_ICON | NIF_MESSAGE | NIF_TIP;
    nid.uCallbackMessage	=	WM_TRAYICON;
    nid.hIcon				=	readyIcon;
    lstrcpy(nid.szTip, "Ready");
    Shell_NotifyIcon(NIM_ADD,&nid);

    loadSettingsFile();

    CreateThread(NULL, 0, loopProcedure, (void*)&nid, 0, NULL);

    while (GetMessage (&messages, NULL, 0, 0)) {
        TranslateMessage(&messages);
        DispatchMessage(&messages);
    }

    return messages.wParam;
}

LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
    switch(message) {
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        case WM_PERCENT: {
            NOTIFYICONDATA nid = *(NOTIFYICONDATA*)lParam;
            int percent = (int)wParam;
            sprintf(nid.szTip, "%d%% Transferred", percent);
            nid.hIcon = transferIcon;
            Shell_NotifyIcon(NIM_MODIFY, &nid);
            return DefWindowProc (hwnd, message, wParam, lParam);
        }
        case WM_STATUS: {
            NOTIFYICONDATA nid = *(NOTIFYICONDATA*)lParam;
            int status = (int)wParam;
            switch(status) {
                case READY:
                    sprintf(nid.szTip, "Ready");
                    nid.hIcon = readyIcon;
                    break;
                case TRANSFER:
                    sprintf(nid.szTip, "0%% Transferred");
                    nid.hIcon = transferIcon;
                    break;
                case FAILED:
                    sprintf(nid.szTip, "Transfer problem. Talk to Keegan.");
                    nid.hIcon = failedIcon;
                    break;
            }
            Shell_NotifyIcon(NIM_MODIFY, &nid);
            return DefWindowProc (hwnd, message, wParam, lParam);
        }
        case WM_TRAYICON:
            return DefWindowProc (hwnd, message, wParam, lParam);
        default:
            return DefWindowProc (hwnd, message, wParam, lParam);
    }
    return 0;
}

DWORD WINAPI loopProcedure(void* nidV) {
    NOTIFYICONDATA nid = *(NOTIFYICONDATA*)nidV;

    while(true) {
        logfile << endl << endl << endl <<"--------------------------------------------------------------------------------";
        performUpdatedFilesRoutine(origin, &nid);
        logfile <<"--------------------------------------------------------------------------------" << endl;
        Sleep(30000);
    }
}

HICON loadIconFromBitmap(string filename) {
    HBITMAP bitmap;
    bitmap = (HBITMAP)LoadImage(NULL, filename.c_str(), IMAGE_BITMAP, 16, 16, LR_LOADFROMFILE);
    if(bitmap == NULL) {
        logfile << "<<<<IMAGE LOAD FAILED>>>>>>" << endl;
    }

    BITMAP bm;
    GetObject(bitmap, sizeof(BITMAP), &bm);
    HBITMAP hbmMask = CreateCompatibleBitmap(GetDC(NULL), bm.bmWidth, bm.bmHeight);

    ICONINFO i;
    i.fIcon = TRUE;
    i.hbmColor = bitmap;
    i.hbmMask = hbmMask;

    HICON icon = CreateIconIndirect(&i);
    DeleteObject(hbmMask);

    return icon;
}

void performUpdatedFilesRoutine(string origin, NOTIFYICONDATA* nid) {
    vector<FILE_INFO> files;
    vector<FILE_INFO> modifiedFiles;
    vector<FILE_INFO> directories;

    FILETIME last = readLastCheckTime();

    logfile << "Enumerating files in " << origin << endl;
    storeDirectoryContents(origin, &files, &directories);
    logfile << "" << files.size() << " files in " << directories.size() << " directories." << endl << endl;

    modifiedFiles = filesModifiedAfterTime(files, last);
    logfile << endl;

    SendMessage(nid->hWnd, WM_STATUS, TRANSFER, (LPARAM)nid);
    if(updateRemoteFiles(modifiedFiles, directories, origin, nid)) {
        SendMessage(nid->hWnd, WM_STATUS, READY, (LPARAM)nid);
        recordCheckTime();
    }
    else {
        SendMessage(nid->hWnd, WM_STATUS, FAILED, (LPARAM)nid);
    }
}

bool updateRemoteFiles(vector<FILE_INFO> modifiedFiles, vector<FILE_INFO> directories, string origin, NOTIFYICONDATA* nid) {
    netbuf* nControl = connectToServer();
    if(nControl) {
        logfile << "Connected to FTP server." << endl;
    }
    else {
        logfile << "Couldn't establish connection, quitting." << endl;
        return 0;
    }
    replicateDirectoryStructure(directories, origin, nControl);
    pushChangedFiles(modifiedFiles, origin, nControl, nid);
    disconnectFromServer(nControl);
    return 1;
}

netbuf* connectToServer() {
    netbuf* nControl;
    FtpInit();

    if(!FtpConnect(server.c_str(), &nControl))
        return (netbuf*)NULL;
    if(!FtpLogin(username.c_str(), password.c_str(), nControl))
        return (netbuf*)NULL;

    return nControl;
}

void replicateDirectoryStructure(vector<FILE_INFO> dirs, string origin, netbuf* nControl) {
    logfile << "Ensuring consistant directory structure... ";
    FtpMkdir("carboniteclone", nControl);
    FtpChdir("carboniteclone", nControl);
    vector<FILE_INFO>::iterator it;
    for(it = dirs.begin(); it != dirs.end(); it++) {
        string mkdirPath = (it->fullPath).substr(origin.length()+1);
        FtpMkdir(mkdirPath.c_str(), nControl);
    }
    logfile << "done." << endl;
}

void pushChangedFiles(vector<FILE_INFO> files, string origin, netbuf* nControl, NOTIFYICONDATA* nid) {
    //This function will eventually send Windows messages as it pushes
    logfile << "Pushing changed files... " << endl;
    vector<FILE_INFO>::iterator it;
    int i = 0;
    int size = files.size();
    for(it = files.begin(); it != files.end(); it++) {
        string localPath = it->fullPath;
        string remotePath = localPath.substr(origin.length()+1);
        FtpPut(localPath.c_str(), remotePath.c_str(), FTPLIB_IMAGE, nControl);
        i++;
        double percentD = (double)i / (double)size * 100.0;
        int percent = (int)percentD;
        logfile << i << "/" << size << " (" << percent << "%) " << (it->findData).cFileName << endl;
        SendMessage(nid->hWnd, WM_PERCENT, percent, (LPARAM)nid);
    }
    logfile << "done." << endl << endl;
}

void disconnectFromServer(netbuf* nControl) {
    FtpQuit(nControl);
}

string readInTempFile() {
    ifstream tempFile;
    string send = "";
    tempFile.open("logfile.txt");
    string line;
    if(tempFile) {
        while(getline(tempFile, line)) {
            send += line + "\n";
        }
    }
    tempFile.close();
    return send;
}

vector<FILE_INFO> filesModifiedAfterTime(vector<FILE_INFO> files, FILETIME time) {
    vector<FILE_INFO> modified;
    vector<FILE_INFO>::iterator it;

    for(it = files.begin(); it != files.end(); it++) {
        if(fileModifiedAfterTime(*it, time))
            modified.push_back(*it);
    }

    // REPORT
    logfile << modified.size() << " files have been modified since " << stringForFileTime(time) << endl;
    if(modified.size() < 20)
        printFindInfoVector(modified);
    else
        logfile << "Too many files to show here." << endl;

    return modified;
}

bool storeDirectoryContents(string origin, vector<FILE_INFO>* files, vector<FILE_INFO>* directories) {
    WIN32_FIND_DATA fdFile;
    HANDLE hFind = NULL;

    string fileMask = origin + "/*.*";

    if((hFind = FindFirstFile(fileMask.c_str(), &fdFile)) == INVALID_HANDLE_VALUE)
        return false;

    do {
        if(strcmp(fdFile.cFileName, ".") != 0 && strcmp(fdFile.cFileName, "..") != 0) {
            FILE_INFO info;
            info.findData = fdFile;
            info.fullPath = origin + "/" + fdFile.cFileName;

            if(fdFile.dwFileAttributes &FILE_ATTRIBUTE_DIRECTORY) {
                directories->push_back(info);
                storeDirectoryContents(info.fullPath, files, directories);
            }
            else {
                files->push_back(info);
            }
        }
    }
    while(FindNextFile(hFind, &fdFile));

    FindClose(hFind);

    return true;
}

void printFindInfoVector(vector<FILE_INFO> input) {
    vector<FILE_INFO>::iterator it;
    for(it = input.begin(); it != input.end(); it++)
        logfile << it->fullPath << endl;
}

void loadSettingsFile() {
    ifstream settingsFile;
    settingsFile.open("settings.txt");
    getline(settingsFile, origin);
    getline(settingsFile, server);
    getline(settingsFile, username);
    getline(settingsFile, password);
}

void recordCheckTime() {
    FILETIME now;
    GetSystemTimeAsFileTime(&now);
    ofstream settingsFile;
    settingsFile.open("record.txt", ios::binary);
    settingsFile.write((char*)&now, sizeof(FILETIME));
    settingsFile.close();
    logfile << "Recorded updated check time of: " << stringForFileTime(now) << endl;
}

FILETIME readLastCheckTime() {
    FILETIME read;
    ifstream settingsFile2;

    settingsFile2.open("record.txt", ios::binary);
    if(!settingsFile2) {
        logfile << "SETTINGS FILE DOESNT EXIST" << endl;
        memset(&read, 0, sizeof(FILETIME));
    }
    else {
        settingsFile2.read((char*)&read, sizeof(FILETIME));
    }
    settingsFile2.close();

    return read;
}

string stringForFileTime(FILETIME in) {
    SYSTEMTIME t;
    FileTimeToSystemTime(&in, &t);
    char buffer[60];
    sprintf(buffer, "%d/%d/%d  %d:%d:%d", t.wMonth, t.wDay, t.wYear, t.wHour, t.wMinute, t.wSecond);

    return string(buffer);
}

bool fileTimeEmpty(FILETIME in) {
    char* tester = (char*)&in;
    for(int i = 0; i < sizeof(FILETIME); i++)
        if(tester[i] != 0)
            return false;
    return true;
}

inline bool fileModifiedAfterTime(FILE_INFO file, FILETIME time) {
    FILETIME lastWrite = file.findData.ftLastWriteTime;
    if(CompareFileTime(&lastWrite, &time) == 1)
        return true;
    else
        return false;
}
